package map;

//TODO: NEEDS IMPROVED COMMENTS
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JPanel;
import javax.swing.Scrollable;

// TODO: SET BACKGROUND COLOR
// TODO: IMPLEMENT CANVAS?
// TODO: MAKE ADD MODES SHOW GHOSTED IMAGES
@SuppressWarnings("serial")
public class MapDisplayPanel extends JPanel implements MouseListener,
        MouseMotionListener, Scrollable, Observer
{
    public static final int FORCE_PREFERRED_SIZE = 0;
    public static final int FILL_AVAILABLE_AREA = 1;
    int displayMode = 0;

    public static final int EDIT_TERRAIN_MODE = 0;
    public static final int ADD_ITEMS_MODE = 1;
    public static final int MOVE_ITEMS_MODE = 2;
    private int manipulationMode = 0;
    private int addTerrain = -1;
    int addItem = -1;

    // Variables about the item
    // currently being dragged, if any
    MapDataItemInstance draggedItem = null;
    int draggedItemXcoordinate = -1;
    int draggedItemYcoordinate = -1;
    int largeItemXOffset = -1;
    int largeItemYOffset = -1;
    int draggedItemXoffsetInPixels = -1; // Where the item was grabbed
    int draggedItemYoffsetInPixels = -1;
    int lastRecordedCursorX = -1; // Used whenever mouse is dragged
    int lastRecordedCursorY = -1;

    private Map map;
    private Color gridColor = Color.BLACK;
    private int preferredEdgeLength = 50;

    public MapDisplayPanel(Map map)
    {
        this.map = map;
        setBackground(Color.BLACK);
        setOpaque(false);
        setDisplayMode(FORCE_PREFERRED_SIZE);
        // setDisplayMode(FILL_AVAILABLE_AREA);
        setManipulationMode(MOVE_ITEMS_MODE);
        addMouseListener(this);
        addMouseMotionListener(this);
        map.addObserver(this);
    }

    public void setDisplayMode(int mode)
    {
        displayMode = mode;
        refactorSize();
    }

    public void setManipulationMode(int mode)
    {
        manipulationMode = mode;
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        refactorSize();
        // g.setColor(backgroundColor);
        // g.fillRect(0, 0, getWidth(), getHeight());
        int squareLength = getSquareLength();
        g.setColor(gridColor);
        g.drawLine(0, 0, squareLength * map.data.getWidth(), 0);
        g.drawLine(0, 0, 0, squareLength * map.data.getHeight());
        for (int x = 0; x < map.data.getWidth(); x++)
        {
            for (int y = 0; y < map.data.getHeight(); y++)
            {
                if (map.data.hasTerrainAt(x, y))
                    g.drawImage(map.getTerrainImage(x, y),
                            x * squareLength + 1, y * squareLength + 1,
                            squareLength - 1, squareLength - 1, this);
                g.drawLine(0, (y + 1) * squareLength,
                        squareLength * map.data.getWidth(), (y + 1)
                                * squareLength);
                g.drawLine((x + 1) * squareLength, 0, (x + 1) * squareLength,
                        squareLength * map.data.getHeight());
            }
        }
        for (int x = 0; x < map.data.getWidth(); x++)
        {
            for (int y = 0; y < map.data.getHeight(); y++)
            {
                if (map.data.hasItemLocatedAt(x, y))
                {
                    if (draggedItemXcoordinate != x
                            || draggedItemYcoordinate != y) // If item isn't
                                                            // being dragged
                    {
                        if (map.data.getItemRotation(x, y) % 2 == 0) // If item
                                                                     // is
                            // oriented
                            // North or
                            // South
                            g.drawImage(map.getItemImage(x, y), x
                                    * squareLength + 1, y * squareLength + 1,
                                    squareLength * map.data.getItemWidth(x, y)
                                            - 1,
                                    squareLength * map.data.getItemHeight(x, y)
                                            - 1, this);
                        else
                            // Item is oriented East or West
                            g.drawImage(map.getItemImage(x, y), x
                                    * squareLength + 1, y * squareLength + 1,
                                    squareLength * map.data.getItemHeight(x, y)
                                            - 1,
                                    squareLength * map.data.getItemWidth(x, y)
                                            - 1, this);
                    }
                    else
                    {
                        if (map.data.getItemRotation(x, y) % 2 == 0) // If item
                                                                     // is
                            // oriented
                            // North or
                            // South
                            g.drawImage(map.getItemImage(x, y),
                                    lastRecordedCursorX
                                            - draggedItemXoffsetInPixels + 1,
                                    lastRecordedCursorY
                                            - draggedItemYoffsetInPixels + 1,
                                    squareLength * map.data.getItemWidth(x, y)
                                            - 1,
                                    squareLength * map.data.getItemHeight(x, y)
                                            - 1, this);
                        else
                            // Item is oriented East or West
                            g.drawImage(map.getItemImage(x, y),
                                    lastRecordedCursorX
                                            - draggedItemXoffsetInPixels + 1,
                                    lastRecordedCursorY
                                            - draggedItemYoffsetInPixels + 1,
                                    squareLength * map.data.getItemHeight(x, y)
                                            - 1,
                                    squareLength * map.data.getItemWidth(x, y)
                                            - 1, this);
                    }

                }
            }
        }
    }

    public void setMap(Map map)
    {
        this.map.deleteObserver(this);
        this.map = map;
        this.map.addObserver(this);
        repaint();
    }

    // Makes Panel Scrollable
    public Dimension getPreferredScrollableViewportSize()
    {
        return super.getPreferredSize();
    }

    public int getScrollableBlockIncrement(Rectangle arg0, int arg1, int arg2)
    {
        return 100;
    }

    public boolean getScrollableTracksViewportHeight()
    {
        return displayMode == FILL_AVAILABLE_AREA;
    }

    public boolean getScrollableTracksViewportWidth()
    {
        return displayMode == FILL_AVAILABLE_AREA;
    }

    public int getScrollableUnitIncrement(Rectangle arg0, int arg1, int arg2)
    {
        return 10;
    }

    public void refactorSize()
    {
        setSize(map.data.getWidth() * preferredEdgeLength + 1,
                map.data.getHeight() * preferredEdgeLength + 1);
        setPreferredSize(new Dimension(map.data.getWidth()
                * preferredEdgeLength + 1, map.data.getHeight()
                * preferredEdgeLength + 1));
    }

    public Map getMap()
    {
        return map;
    }

    public void setAddTerrain(int type)
    {
        addTerrain = type;
    }

    public void setAddItem(int type)
    {
        addItem = type;
    }

    /**
     * Interprets left-clicks and right-clicks according to the current
     * manipulation mode<br>
     * <ul>
     * </ul>
     * EDIT_TERRAIN_MODE:
     * <ul>
     * <li>Left-click: Add selected terrain square type</li>
     * <li>Ctrl + Left-click: Remove terrain tile at cursor location</li>
     * <li>Right-click: Rotate terrain tile at cursor location</li>
     * </ul>
     * ADD_ITEMS_MODE:
     * <ul>
     * <li>Left-click: Add selected item type</li>
     * <li>Ctrl + Left-click: Remove item at cursor location</li>
     * </ul>
     * MOVE_ITEMS_MODE:<br>
     * <ul>
     * <li>Right-click: Rotate terrain tile at cursor location</li>
     * </ul>
     */
    public void mouseClicked(MouseEvent e)
    {
    }

    /** Not currently in use */
    public void mouseEntered(MouseEvent e)
    {
    }

    /** Not currently in use */
    public void mouseExited(MouseEvent e)
    {
    }

    /**
     * Grabs item at square cursor is currently over, if any. Dragging the mouse
     * will move that item
     */
    public void mousePressed(MouseEvent e)
    {
        // TODO: CLEAN THIS METHOD
        int squareLength = getSquareLength();
        int x = e.getX() / squareLength;
        int y = e.getY() / squareLength;
        if (e.getX() % squareLength == 0 || e.getX() % squareLength == 0)
            // The click is on a grid line; do nothing
            return;
        if (x >= map.data.getWidth() || y >= map.data.getHeight())
            // The click is off the grid; do nothing
            return;
        switch (manipulationMode)
        {
        case EDIT_TERRAIN_MODE:
            if (e.getButton() == MouseEvent.BUTTON1)
            {
                boolean ctrlPressed = MouseEvent.getMouseModifiersText(
                        e.getModifiers()).contains("Ctrl");
                if (ctrlPressed)
                    map.data.removeTerrain(x, y);
                else
                    map.data.setTerrain(x, y, addTerrain, 0);
            }
            else if (e.getButton() == MouseEvent.BUTTON3
                    && map.data.hasTerrainAt(x, y))
                map.data.rotateTerrainCW(x, y);
            break;
        case ADD_ITEMS_MODE:
            if (e.getButton() == MouseEvent.BUTTON1)
            {
                boolean ctrlPressed = MouseEvent.getMouseModifiersText(
                        e.getModifiers()).contains("Ctrl");
                if (ctrlPressed)
                    map.data.removeItem(x, y);
                else
                    // TODO: CONSIDER REQUIRING THAT NO ITEM ALREADY BE HERE
                    map.data.setItem(x, y, addItem, 0, map.library
                            .getItemEntry(addItem).getWidth(), map.library
                            .getItemEntry(addItem).getHeight());
            }
            else if (e.getButton() == MouseEvent.BUTTON3
                    && map.data.hasItemAt(x, y))
                map.data.rotateItemCW(x, y); // Rotate on right mouse button
            break;
        case MOVE_ITEMS_MODE:
            if (map.data.hasItemAt(x, y))
            {
                if (e.getButton() == MouseEvent.BUTTON1)
                {
                    int[] arrayLocation = map.data.getItemLocation(x, y);
                    draggedItemXoffsetInPixels = e.getX() % squareLength
                            - (arrayLocation[0] - x) * squareLength; // Where on
                                                                     // the
                                                                     // square
                                                                     // the item
                                                                     // was
                                                                     // grabbed
                    draggedItemYoffsetInPixels = e.getY() % squareLength
                            - (arrayLocation[1] - y) * squareLength;
                    if (draggedItemXoffsetInPixels == 0
                            || draggedItemYoffsetInPixels == 0)
                        // The click is on a grid line; do nothing
                        return;
                    draggedItemXcoordinate = arrayLocation[0];
                    draggedItemYcoordinate = arrayLocation[1];
                    largeItemXOffset = x - arrayLocation[0];
                    largeItemYOffset = y - arrayLocation[1];
                    mouseDragged(e); // Assume the mouse is about to be dragged
                }
                else if (e.getButton() == MouseEvent.BUTTON3
                        && map.data.hasItemAt(x, y))
                    map.data.rotateItemCW(x, y); // Rotate on right mouse button
            }
            break;
        }
    }

    /**
     * Relocates a dragged item, if applicable, and then repaints; otherwise,
     * simply calls repaint
     */
    public void mouseReleased(MouseEvent e)
    {
        if (manipulationMode == MOVE_ITEMS_MODE)
        {
            int squareLength = getSquareLength();
            if (e.getX() % squareLength == 0 || e.getX() % squareLength == 0)
                // The click is on a grid line; do nothing
                return;
            int x = e.getX() / squareLength;
            int y = e.getY() / squareLength;
            if (map.data.hasItemAt(draggedItemXcoordinate,
                    draggedItemYcoordinate)
                    && (draggedItemXcoordinate != x || draggedItemYcoordinate != y))
            {
                int width = map.data.getItemWidth(draggedItemXcoordinate,
                        draggedItemYcoordinate);
                int height = map.data.getItemHeight(draggedItemXcoordinate,
                        draggedItemYcoordinate);
                int rotation = map.data.getItemRotation(draggedItemXcoordinate,
                        draggedItemYcoordinate);
                int newX = x - largeItemXOffset;
                int newY = y - largeItemYOffset;
                if ((rotation % 2 == 0 && newX >= 0 && newY >= 0
                        && newX + width <= map.data.getWidth() && newY + height <= map.data
                        .getHeight())
                        || rotation % 2 == 1
                        && newX >= 0
                        && newY >= 0
                        && newX + height <= map.data.getWidth()
                        && newY + width <= map.data.getHeight())
                {
                    map.data.moveItem(draggedItemXcoordinate,
                            draggedItemYcoordinate, newX, newY);
                }

            }
        }
        draggedItemXcoordinate = -1;
        draggedItemYcoordinate = -1;
        repaint();
    }

    /**
     * Gets the square length in pixels to be used when painting the map
     * 
     * @return The side length of a square on the painted panel
     */
    private int getSquareLength()
    {
        if (displayMode == FORCE_PREFERRED_SIZE)
            return preferredEdgeLength;
        else
        {
            int panelWidth = getWidth();
            int panelHeight = getHeight();

            int pixelsPerWidth = panelWidth / map.data.getWidth();
            int pixelsPerHeight = panelHeight / map.data.getHeight();

            if (pixelsPerHeight < pixelsPerWidth)
                return pixelsPerHeight;
            return pixelsPerWidth;
        }
    }

    public int getManipulationMode()
    {
        return manipulationMode;
    }

    /**
     * When the mouse is dragged, it is likely because an item is being moved.
     * This method saves the mouse position and repaints the Panel so that the
     * item's visual position may be updated.
     */
    public void mouseDragged(MouseEvent e)
    {
        lastRecordedCursorX = e.getX();
        lastRecordedCursorY = e.getY();
        repaint();
    }

    /** Not currently in use */
    public void mouseMoved(MouseEvent e)
    {
    }

    @Override
    public void update(Observable o, Object arg)
    {
        if (arg.equals(MapDisplayPanel.class))
            repaint();
    }
}
